﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Xml;
using NUnit.Framework;
using PropertyExpression.ControllableQuery;
using PropertyExpression.ControllableQuery.Test;

namespace PropertyExpression.ControllableQueryTool.Test
{
    [TestFixture]
    public class MainTest
    {
        [Test]
        public void Test()
        {
            foreach (var type in typeof (PartLinkQuery).Assembly.GetTypes())
            {
                foreach (var @interface in type.GetInterfaces())
                {
                    if (InterfaceIsGenericAndEquals(@interface, typeof(IQuery<>)))
                    {
                        ProcessQueryType(type);
                    }
                }
            }
        }

        private static void ProcessQueryType(Type queryType)
        {
            foreach (var querInstance in InvokeConstructor(queryType))
            {
                var queryText = ((IQuery)querInstance).TransformText();
                var paramList = new List<string>();
                ProcessQueryText(queryText, paramList);

                var executionPlanXml = WithSqlCommand(
                    sqlCommand =>
                        {
                            sqlCommand.CommandText = "SET SHOWPLAN_XML ON";
                            sqlCommand.ExecuteNonQuery();

                            var paramStringBuilder = GetDeclareVarbinaryParams(paramList);
                            sqlCommand.CommandText =
                                string.Format(@"{0}
{1}",
                                              paramStringBuilder,
                                              queryText);
                            var result = sqlCommand.ExecuteScalar();

                            sqlCommand.CommandText = "SET SHOWPLAN_XML OFF";
                            sqlCommand.ExecuteNonQuery();

                            return result;
                        }
                    );

                var xmlDocument = new XmlDocument();
                xmlDocument.LoadXml((string)executionPlanXml);
                var xmlNamespaceManager = new XmlNamespaceManager(xmlDocument.NameTable);
                xmlNamespaceManager.AddNamespace("a", "http://schemas.microsoft.com/sqlserver/2004/07/showplan");

                //TODO:
                Console.WriteLine(queryText);
                Console.WriteLine("#################");

                foreach (var param in paramList)
                {
                    var innerText = xmlDocument.SelectSingleNode(
                        string.Format(@"//a:ColumnReference[@Column='{0}']/ancestor::a:Convert/@DataType",
                                      param), xmlNamespaceManager).InnerText;

                    Console.WriteLine("{0} {1}", param, innerText);
                }

                Console.WriteLine("****************************");

            }
        }

        private static void ProcessQueryText(string queryText, ICollection<string> paramList)
        {
            var paramStringBuilder = GetDeclareVarbinaryParams(paramList);

            WithSqlCommand(sqlCommand =>
                     {
                         sqlCommand.CommandText =
                             @"SET LANGUAGE english 

BEGIN TRY 
    exec sp_executesql @Query
END
TRY 
BEGIN
CATCH 
    SELECT
        ERROR_NUMBER() AS ErrorNumber,
        ERROR_MESSAGE() AS ErrorMessage,
        ERROR_LINE() AS ErrorLine
END CATCH";
                         sqlCommand.Parameters.Add(
                             new SqlParameter
                                 {
                                     ParameterName = "@Query",
                                     SqlDbType = SqlDbType.NVarChar,
                                     Size = -1,
                                     Value = string.Format(@"SET FMTONLY ON;
{1}
{0}",
                                                           queryText,
                                                           paramStringBuilder)
                                 }
                             );

                         Action action;
                         using (var dataReader = sqlCommand.ExecuteReader())
                         {
                             if (dataReader.Read())
                             {
                                 var errorNumber = (int) dataReader["ErrorNumber"];
                                 var errorMessage = (string) dataReader["ErrorMessage"];
                                 if (errorNumber == 137)
                                 {
                                     action =
                                         () =>
                                             {
                                                 var substring = errorMessage.Substring(
                                                     "Must declare the scalar variable \"".Length);
                                                 paramList.Add(substring.Substring(0, substring.Length - "\".".Length));
                                                 ProcessQueryText(queryText, paramList);
                                             };
                                 }
                                 else
                                 {
                                     throw new ApplicationException(
                                         string.Format("{0} Error line: {1}.",
                                                       errorMessage,
                                                       dataReader["ErrorLine"]
                                             )
                                         );
                                 }
                             }
                             else
                             {
                                 action = delegate { };
                             }
                         }
                         action();
                     }
                );
        }

        private static StringBuilder GetDeclareVarbinaryParams(IEnumerable<string> paramList)
        {
            var paramStringBuilder = new StringBuilder();
            foreach (var param in paramList)
            {
                paramStringBuilder.AppendFormat("DECLARE {0} VARBINARY(MAX)", param);
                paramStringBuilder.AppendLine();
            }
            return paramStringBuilder;
        }

        private static TResult WithSqlCommand<TResult>(Func<SqlCommand, TResult> func)
        {
            using (var connection = new SqlConnection(
                @"Data Source=(local)\MSSQLSERVER2008;Initial Catalog=ControllableQuery;Integrated Security=True"))
            {
                connection.Open();

                using (var sqlCommand = new SqlCommand())
                {
                    sqlCommand.Connection = connection;

                    return func(sqlCommand);
                }
            }
        }

        private static void WithSqlCommand(Action<SqlCommand> action)
        {
            WithSqlCommand(
                command =>
                    {
                        action(command);
                        return new {};
                    }
                );
        }

        private static IEnumerable<IEnumerable<object>> GetArgRepository(IEnumerable<ParameterInfo> parameterInfos)
        {
            var argRepository = new List<List<object>>();
            foreach (var parameterInfo in parameterInfos)
            {
                var nextArgRepository = new List<List<object>>();
                foreach (var choiceInstance in GetChoiceInstances(parameterInfo.ParameterType))
                {
                    if (argRepository.Count == 0)
                    {
                        nextArgRepository.Add(new List<object> {choiceInstance});
                    }
                    else
                    {
                        foreach (var argList in argRepository)
                        {
                            argList.Add(choiceInstance);
                            nextArgRepository.Add(argList);
                        }
                    }
                }
                argRepository = nextArgRepository;
            }
            return argRepository;
        }

        private static IEnumerable<object> GetChoiceInstances(Type type)
        {
            if (InterfaceIsGenericAndEquals(type, typeof(IParams<>)))
            {
                return new[]
                           {
                               GetConstructorInfo(typeof (Params<>).MakeGenericType(
                                   type.GetGenericArguments().ToArray())
                                   ).Invoke(new object[] {})
                           };
            }
            else if (typeof(bool).Equals(type))
            {
                return new object[] { false, true };
            }
            else if (type.IsInterface)
            {
                return GetImplementations(type).SelectMany(InvokeConstructor);
            }
            else
            {
                throw new ApplicationException();
            }
        }

        private class Params<T>: IParams<T>
        {            
        }

        private static IEnumerable<object> InvokeConstructor(Type implementationType)
        {
            var constructorInfo = GetConstructorInfo(implementationType);
            var parameterInfos = constructorInfo.GetParameters();
            if (parameterInfos.Length == 0)
            {
                return new[] { constructorInfo.Invoke(new object[] {}) };
            }
            return GetArgRepository(parameterInfos)
                .Select(argList => constructorInfo.Invoke(argList.ToArray()));
        }

        private static IEnumerable<Type> GetImplementations(Type interfaceType)
        {
            foreach (var type in interfaceType.Assembly.GetTypes())
            {
                foreach (var @interface in type.GetInterfaces())
                {
                    if (interfaceType.Equals(@interface))
                    {
                        yield return type;
                    }
                }
            }
        }

        private static ConstructorInfo GetConstructorInfo(Type type)
        {
            //TODO: написвать сообщение для эксепшена, если конструктор не один
            return type.GetConstructors().Single();
        }

        private static bool InterfaceIsGenericAndEquals(Type type1, Type typ2)
        {
            return type1.IsGenericType && typ2.Equals(type1.GetGenericTypeDefinition());
        }
    }
}